﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using IRongs.Wbt.Common.Entities;
using System.Security.Cryptography;
using System.Web;
using IRongs.Wbt.Common.Attributes;
using System.Collections.Specialized;
using Newtonsoft.Json;

namespace IRongs.Wbt.Common.Auth
{
    [Serializable]
    public abstract class OAuthBase : IOAuthable
    {

        #region Properties

        public abstract string ConsumerKey { get; }

        public abstract string ConsumerSecret { get; }

        public string Token { get; private set; }

        public string TokenSecret { get; private set; }

        public string Verifier { get; private set; }

        public abstract string RequestTokenUrl { get; }

        public abstract string AuthorizeUrl { get; }

        public abstract string AccessTokenUrl { get; }

        public string UserId { get; private set; }

        #endregion

        #region IRequestable

        /// <summary>
        /// Sends the request to specified URL, the user authentication info will be included automatically.
        /// </summary>
        /// <typeparam name="T">The returned JSON data will be translated to this type of entity.</typeparam>
        /// <param name="url">The URL.</param>
        /// <param name="method">The method.</param>
        /// <param name="parameters">The parameters.</param>
        /// <returns>
        /// The object information returned from the requests.
        /// </returns>
        public T SendRequest<T>(string url, Method method, IEnumerable<QueryParameter> parameters) where T : IEntity
        {
            string ret = SendRequest(url, method, parameters);

            // Convert the returned JSON/XML format string to entities.
            T entity = JsonConvert.DeserializeObject<T>(ret);

            return entity;
        }

        /// <summary>
        /// Sends the request to specified URL, the user authentication info will be included automatically.
        /// </summary>
        /// <param name="url">The URL.</param>
        /// <param name="method">The method.</param>
        /// <param name="parameters">The parameters.</param>
        public string SendRequest(string url, Method method, IEnumerable<QueryParameter> parameters)
        {
            string data = string.Empty;
            if (method == Method.POST)
            {
                data = NormalizeRequestParameters(parameters, true);
                data = string.Format("{0}&source={1}", data, this.UrlEncode(HttpUtility.UrlEncode(this.ConsumerKey)));
                url = string.Format("{0}{2}{1}", url, data, (url.IndexOf("?") > 0) ? "&" : "?");
            }
            else if (method == Method.GET)
            {
                data = string.Format("{0}&source={1}", NormalizeRequestParameters(parameters, false), this.ConsumerKey);
            }

            Uri uri = new Uri(url);

            string querystring = string.Empty;
            string outUrl = string.Empty;
            //Generate Signature
            string sig = this.GenerateSignature(uri, method, out outUrl, out querystring);
            querystring = string.Format("{0}&oauth_signature={1}", querystring, HttpUtility.UrlEncode(sig));

            if (method == Method.POST)
            {
                data = querystring;
                querystring = string.Empty;
            }

            if (querystring.Length > 0)
            {
                outUrl += "?";
            }

            string ret = string.Empty;
            if (method == Method.POST || method == Method.GET)
                ret = WebRequestHelper.WebRequest(outUrl + querystring, data, method);

            return ret;
        }

        #endregion

        #region IOAuthable

        /// <summary>
        /// Gets the request token.
        /// </summary>
        /// <returns>The Url to get do secure authentication.</returns>
        public string GetRequestToken()
        {
            string ret = null;
            string response = SendRequest(this.RequestTokenUrl, Method.GET, null);
            if (response.Length > 0)
            {
                NameValueCollection qs = HttpUtility.ParseQueryString(response);
                if (qs["oauth_token"] != null)
                {
                    this.Token = qs["oauth_token"];
                    this.TokenSecret = qs["oauth_token_secret"];
                    ret = string.Format("{0}?oauth_token={1}", this.AuthorizeUrl, this.Token);
                }
            }

            return ret;
        }

        /// <summary>
        /// Gets the access token.
        /// </summary>
        /// <param name="verifier">The verifier.</param>
        public void GetAccessToken(string verifier)
        {
            this.Verifier = verifier;

            string response = SendRequest(this.AccessTokenUrl, Method.GET, null);

            if (response.Length > 0)
            {
                NameValueCollection qs = HttpUtility.ParseQueryString(response);
                if (qs["oauth_token"] != null)
                {
                    this.Token = qs["oauth_token"];
                }
                if (qs["oauth_token_secret"] != null)
                {
                    this.TokenSecret = qs["oauth_token_secret"];
                }
                if (qs["user_id"] != null)
                {
                    this.UserId = qs["user_id"];
                }
            }
        }

        #endregion

        #region Private Fields

        protected const string OAuthVersion = "1.0";
        protected const string OAuthParameterPrefix = "oauth_";

        //
        // List of know and used oauth parameters' names
        //        
        protected const string OAuthConsumerKeyKey = "oauth_consumer_key";
        protected const string OAuthCallbackKey = "oauth_callback";
        protected const string OAuthVersionKey = "oauth_version";
        protected const string OAuthSignatureMethodKey = "oauth_signature_method";
        protected const string OAuthSignatureKey = "oauth_signature";
        protected const string OAuthTimestampKey = "oauth_timestamp";
        protected const string OAuthNonceKey = "oauth_nonce";
        protected const string OAuthTokenKey = "oauth_token";
        protected const string OAuthTokenSecretKey = "oauth_token_secret";
        protected const string OAuthVerifier = "oauth_verifier";

        protected const string HMACSHA1SignatureType = "HMAC-SHA1";
        protected const string PlainTextSignatureType = "PLAINTEXT";
        protected const string RSASHA1SignatureType = "RSA-SHA1";

        protected Random random = new Random();

        protected string unreservedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~";

        #endregion

        #region Parameters Handling

        /// <summary>
        /// Internal function to cut out all non oauth query string parameters (all parameters not begining with "oauth_")
        /// </summary>
        /// <param name="parameters">The query string part of the Url</param>
        /// <returns>
        /// A list of QueryParameter each containing the parameter name and value
        /// </returns>
        private List<QueryParameter> GetOAuthQueryParameters(string parameters)
        {
            if (parameters.StartsWith("?"))
            {
                parameters = parameters.Remove(0, 1);
            }

            List<QueryParameter> result = new List<QueryParameter>();

            if (!string.IsNullOrEmpty(parameters))
            {
                string[] p = parameters.Split('&');
                foreach (string s in p)
                {
                    if (!string.IsNullOrEmpty(s) && !s.StartsWith(OAuthParameterPrefix))
                    {
                        if (s.IndexOf('=') > -1)
                        {
                            string[] temp = s.Split('=');
                            result.Add(new QueryParameter(temp[0], temp[1]));
                        }
                        else
                        {
                            result.Add(new QueryParameter(s, string.Empty));
                        }
                    }
                }
            }

            return result;
        }

        /// <summary>
        /// This is a different Url Encode implementation since the default .NET one outputs the percent encoding in lower case.
        /// While this is not a problem with the percent encoding spec, it is used in upper case throughout OAuth
        /// </summary>
        /// <param name="value">The value to Url encode</param>
        /// <returns>Returns a Url encoded string</returns>
        protected string UrlEncode(string value)
        {
            StringBuilder result = new StringBuilder(value.Length * 2);

            foreach (char symbol in value)
            {
                if (unreservedChars.IndexOf(symbol) != -1)
                {
                    result.Append(symbol);
                }
                else
                {
                    result.Append('%' + String.Format("{0:X2}", (int)symbol));
                }
            }

            return result.ToString();
        }

        /// <summary>
        /// Normalizes the request parameters according to the spec
        /// </summary>
        /// <param name="parameters">The list of parameters already sorted</param>
        /// <param name="encoded">if set to <c>true</c> [encoded].</param>
        /// <returns>
        /// a string representing the normalized parameters.
        /// </returns>
        protected string NormalizeRequestParameters(IEnumerable<QueryParameter> parameters, bool encoded)
        {
            if (parameters == null || !parameters.Any())
                return string.Empty;

            StringBuilder sb = new StringBuilder(500);
            foreach (QueryParameter parameter in parameters)
            {
                sb.AppendFormat("{0}={1}&", parameter.Name, encoded ? this.UrlEncode(HttpUtility.UrlEncode(parameter.Value)) : parameter.Value);
            }

            int realLength = sb.Length - 1;
            if (realLength < 0) return string.Empty;
            return sb.ToString(0, realLength);
        }

        #endregion

        #region Generate Signature

        /// <summary>
        /// Helper function to compute a hash value
        /// </summary>
        /// <param name="hashAlgorithm">The hashing algoirhtm used. If that algorithm needs some initialization, like HMAC and its derivatives, they should be initialized prior to passing it to this function</param>
        /// <param name="data">The data to hash</param>
        /// <returns>a Base64 string of the hash value</returns>
        private string ComputeHash(HashAlgorithm hashAlgorithm, string data)
        {
            if (hashAlgorithm == null)
            {
                throw new ArgumentNullException("hashAlgorithm");
            }

            if (string.IsNullOrEmpty(data))
            {
                throw new ArgumentNullException("data");
            }

            byte[] dataBuffer = System.Text.Encoding.ASCII.GetBytes(data);
            byte[] hashBytes = hashAlgorithm.ComputeHash(dataBuffer);

            return Convert.ToBase64String(hashBytes);
        }

        /// <summary>
        /// Generate the signature base that is used to produce the signature.
        /// </summary>
        /// <param name="url">The full url that needs to be signed including its non OAuth url parameters</param>
        /// <param name="method">The method.</param>
        /// <param name="signatureType">The signature type. To use the default values use <see cref="OAuthBase.SignatureTypes">OAuthBase.SignatureTypes</see>.</param>
        /// <param name="normalizedUrl">The normalized URL.</param>
        /// <param name="normalizedRequestParameters">The normalized request parameters.</param>
        /// <returns>The signature base.</returns>
        private string GenerateSignatureBase(Uri url, Method method, string signatureType, out string normalizedUrl, out string normalizedRequestParameters)
        {
            if (this.Token == null)
            {
                this.Token = string.Empty;
            }

            if (this.TokenSecret == null)
            {
                this.TokenSecret = string.Empty;
            }

            if (string.IsNullOrEmpty(this.ConsumerKey))
            {
                throw new ArgumentNullException("ComsumerKey");
            }
            if (string.IsNullOrEmpty(signatureType))
            {
                throw new ArgumentNullException("signatureType");
            }

            normalizedUrl = null;
            normalizedRequestParameters = null;

            List<QueryParameter> parameters = GetOAuthQueryParameters(url.Query);
            parameters.Add(new QueryParameter(OAuthVersionKey, OAuthVersion));
            parameters.Add(new QueryParameter(OAuthNonceKey, this.GenerateNonce()));
            parameters.Add(new QueryParameter(OAuthTimestampKey, this.GenerateTimeStamp()));
            parameters.Add(new QueryParameter(OAuthSignatureMethodKey, signatureType));
            parameters.Add(new QueryParameter(OAuthConsumerKeyKey, this.ConsumerKey));

            if (!string.IsNullOrEmpty(this.Token))
            {
                parameters.Add(new QueryParameter(OAuthTokenKey, this.Token));
            }
            if (!string.IsNullOrEmpty(this.Verifier))
            {
                parameters.Add(new QueryParameter(OAuthVerifier, this.Verifier));
            }

            parameters.Sort(new QueryParameterComparer());

            normalizedUrl = string.Format("{0}://{1}", url.Scheme, url.Host);
            if (!((url.Scheme == "http" && url.Port == 80) || (url.Scheme == "https" && url.Port == 443)))
            {
                normalizedUrl += ":" + url.Port;
            }
            normalizedUrl += url.AbsolutePath;
            normalizedRequestParameters = NormalizeRequestParameters(parameters, false);

            StringBuilder signatureBase = new StringBuilder();
            signatureBase.AppendFormat("{0}&", EnumStringAttribute.GetValue(method).ToUpper());
            signatureBase.AppendFormat("{0}&", UrlEncode(normalizedUrl));
            signatureBase.AppendFormat("{0}", UrlEncode(normalizedRequestParameters));

            return signatureBase.ToString();
        }

        /// <summary>
        /// Generate the signature value based on the given signature base and hash algorithm
        /// </summary>
        /// <param name="signatureBase">The signature based as produced by the GenerateSignatureBase method or by any other means</param>
        /// <param name="hash">The hash algorithm used to perform the hashing. If the hashing algorithm requires initialization or a key it should be set prior to calling this method</param>
        /// <returns>A base64 string of the hash value</returns>
        protected string GenerateSignatureUsingHash(string signatureBase, HashAlgorithm hash)
        {
            return ComputeHash(hash, signatureBase);
        }

        /// <summary>
        /// Generates a signature using the HMAC-SHA1 algorithm.
        /// </summary>
        /// <param name="url">The full url that needs to be signed including its non OAuth url parameters</param>
        /// <param name="method">The http method used. Must be a valid HTTP method verb (POST,GET,PUT, etc)</param>
        /// <param name="normalizedUrl">The normalized URL.</param>
        /// <param name="normalizedRequestParameters">The normalized request parameters.</param>
        /// <returns>A base64 string of the hash value.</returns>
        protected string GenerateSignature(Uri url, Method method, out string normalizedUrl, out string normalizedRequestParameters)
        {
            return GenerateSignature(url, method, SignatureTypes.HMACSHA1, out normalizedUrl, out normalizedRequestParameters);
        }

        /// <summary>
        /// Generates a signature using the specified signatureType.
        /// </summary>
        /// <param name="url">The full url that needs to be signed including its non OAuth url parameters</param>
        /// <param name="method">The http method used. Must be a valid HTTP method verb (POST,GET,PUT, etc)</param>
        /// <param name="signatureType">The type of signature to use</param>
        /// <param name="normalizedUrl">The normalized URL.</param>
        /// <param name="normalizedRequestParameters">The normalized request parameters.</param>
        /// <returns>A base64 string of the hash value.</returns>
        protected string GenerateSignature(Uri url, Method method, SignatureTypes signatureType, out string normalizedUrl, out string normalizedRequestParameters)
        {
            normalizedUrl = null;
            normalizedRequestParameters = null;

            switch (signatureType)
            {
                case SignatureTypes.PLAINTEXT:
                    return HttpUtility.UrlEncode(string.Format("{0}&{1}", this.ConsumerSecret, this.TokenSecret));
                case SignatureTypes.HMACSHA1:
                    string signatureBase = GenerateSignatureBase(url, method, HMACSHA1SignatureType, out normalizedUrl, out normalizedRequestParameters);

                    HMACSHA1 hmacsha1 = new HMACSHA1();
                    hmacsha1.Key = Encoding.ASCII.GetBytes(string.Format("{0}&{1}", UrlEncode(this.ConsumerSecret), string.IsNullOrEmpty(this.TokenSecret) ? "" : UrlEncode(this.TokenSecret)));

                    return GenerateSignatureUsingHash(signatureBase, hmacsha1);
                case SignatureTypes.RSASHA1:
                    throw new NotImplementedException();
                default:
                    throw new ArgumentException("Unknown signature type", "signatureType");
            }
        }

        /// <summary>
        /// Generate the timestamp for the signature.
        /// </summary>
        /// <returns></returns>
        protected virtual string GenerateTimeStamp()
        {
            // Default implementation of UNIX time of the current UTC time
            TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
            return Convert.ToInt64(ts.TotalSeconds).ToString();
        }

        /// <summary>
        /// Generate a nonce.
        /// </summary>
        /// <returns></returns>
        protected virtual string GenerateNonce()
        {
            // Just a simple implementation of a random number between 123400 and 9999999
            return random.Next(123400, 9999999).ToString();
        }

        #endregion

        #region Inner types

        /// <summary>
        /// Provides a predefined set of algorithms that are supported officially by the protocol
        /// </summary>
        protected enum SignatureTypes
        {
            HMACSHA1,
            PLAINTEXT,
            RSASHA1
        }

        #endregion

    }
}
